package com.javacoo.junit.generator.internal;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Pattern;

import com.google.common.base.CharMatcher;
import com.javacoo.junit.generator.api.ImplementFinder;
import com.javacoo.junit.generator.internal.data.PrimitiveType;
import com.javacoo.junit.generator.internal.data.WrapperType;
import com.javacoo.junit.generator.internal.util.StrKit;
import com.javacoo.xkernel.spi.ExtensionLoader;

import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import static com.javacoo.junit.generator.internal.util.Formats.BLANK_1;
import static com.javacoo.junit.generator.internal.util.Formats.BLANK_4;
import static com.javacoo.junit.generator.internal.util.Formats.BLANK_8;
import static com.javacoo.junit.generator.internal.util.Formats.RT_1;

/**
 * 方法帮助类
 * <li></li>
 *
 * @author: duanyong@jccfc.com
 * @since: 2021/1/7 15:48
 */
public class MethodHelper {
    static final String[] excludeModifiers = {"static", "abstract", "transient", "volatile"};

    private MethodHelper() {
        throw new IllegalStateException("Utility class");
    }

    public static boolean filterMethod(Method method) {
        if (method.getName().startsWith("lambda$")) {
            return false;
        }

        if (Modifier.isStatic(method.getModifiers()) && Pattern.matches("access\\$\\d+", method.getName())) {
            return false;
        }
        Class<?> declaringClass = method.getDeclaringClass();

        if (method.getTypeParameters().length > 0) {
            return true;
        }

        TypeVariable<? extends Class<?>>[] classTypeParameters = declaringClass.getTypeParameters();
        for (Type methodParamType : method.getGenericParameterTypes()) {
            for (TypeVariable<? extends Class<?>> classTypeParameter : classTypeParameters) {
                if (methodParamType.getTypeName().equals(classTypeParameter.getTypeName())) {
                    return true;
                }
            }
        }

        List<Type> genericSuperTypes = new ArrayList<>();

        for (Type type : declaringClass.getGenericInterfaces()) {
            genericSuperTypes.add(type);
        }
        genericSuperTypes.add(declaringClass.getGenericSuperclass());

        for (Type type : genericSuperTypes) {
            if (type instanceof ParameterizedTypeImpl) {
                final Class<?> rawType = ((ParameterizedTypeImpl) type).getRawType();
                TypeVariable<? extends Class<?>>[] interfaceTypeParameters = rawType.getTypeParameters();
                List<TypeVariable<? extends Class<?>>> interfaceTypeParametersList = Arrays.asList(interfaceTypeParameters);

                for (Method interfaceMethod : rawType.getDeclaredMethods()) {
                    boolean useInterfaceTypeParameter = false;
                    List<Type> interfaceMethodGenericTypes = new ArrayList<>(Arrays.asList(interfaceMethod.getGenericParameterTypes()));
                    interfaceMethodGenericTypes.add(interfaceMethod.getGenericReturnType());

                    for (Type interfaceMethodGenericType : interfaceMethodGenericTypes) {
                        for (TypeVariable<? extends Class<?>> typeVariable : interfaceTypeParametersList) {
                            if (typeVariable.getTypeName().equals(interfaceMethodGenericType.getTypeName())) {
                                useInterfaceTypeParameter = true;
                                break;
                            }
                        }
                        if (useInterfaceTypeParameter) {
                            break;
                        }
                    }

                    if (useInterfaceTypeParameter) {
                        if (methodEqualsIgnoringDeclaringClass(method, interfaceMethod)) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

    static boolean methodEqualsIgnoringDeclaringClass(Method m1, Method m2) {
        if (m1.getName().equals(m2.getName()) && m1.getReturnType().equals(m2.getReturnType())) {
            Class<?>[] params1 = m1.getParameterTypes();
            Class<?>[] params2 = m2.getParameterTypes();
            if (params1.length == params2.length) {
                for (int i = 0; i < params1.length; i++) {
                    if (params1[i] != params2[i]) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }

    static boolean isDeprecated(Executable executable) {
        Deprecated annotation = executable.getAnnotation(Deprecated.class);
        if (annotation != null) {
            return true;
        } else {
            return false;
        }
    }

    public static <T extends Executable> String needTryCatch(T executable) {
        String hasException = null;
        boolean checkedException = false;
        boolean throwable = false;
        Class[] exceptions = executable.getExceptionTypes();
        if (exceptions.length > 0) {
            for (Class exception : exceptions) {
                if (!RuntimeException.class.isAssignableFrom(exception)) {
                    checkedException = true;

                    if (!Exception.class.isAssignableFrom(exception)) {
                        throwable = true;
                        break;
                    }
                }
            }
        }

        if (checkedException) {
            hasException = "Exception";
        }
        if (throwable) {
            hasException = "Throwable";
        }
        return hasException;
    }

    static CtBehavior convertToCt(Executable executable, CtClass ctClass) {
        Class[] parameterTypes = executable.getParameterTypes();

        CtClass[] params = new CtClass[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            params[i] = getCtClass(parameterTypes[i]);
        }
        CtBehavior behavior = null;
        try {
            if (executable instanceof Constructor) {
                behavior = ctClass.getDeclaredConstructor(params);
            } else {
                behavior = ctClass.getDeclaredMethod(executable.getName(), params);
            }
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return behavior;
    }

    public static CtClass getCtClass(Class<?> c) {
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = null;

        InputStream in = ObjectFactory.getResourceInputStream(c.getName().replace('.', '/') + ".class");
        if (in != null) {
            try {
                ctClass = cp.makeClass(in, false);
                ctClass.defrost();
            } catch (Exception e) {
            }
            if (ctClass != null) {
                return ctClass;
            }
        }

        try {
            ctClass = cp.getCtClass(c.getName());
            ctClass.defrost();
        } catch (Exception e) {
        }
        return Objects.requireNonNull(ctClass);
    }

    public static String[] getExpectedVarNames(Executable method, Class<?> clazz) {
        String[] paramNames = new String[method.getParameterTypes().length];

        if(clazz.isInterface()){
            for(int i=0;i<method.getParameterTypes().length;i++){
                String simpleName = method.getParameterTypes()[i].getSimpleName();
                paramNames[i] = StrKit.UnCapsFirst(simpleName);
            }
            return paramNames;
        }

        CtBehavior behavior = convertToCt(method, getCtClass(clazz));
        if (behavior == null) {
            return paramNames;
        }
        MethodInfo methodInfo = behavior.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        if (codeAttribute == null) {
            return paramNames;
        }
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute
            .getAttribute(LocalVariableAttribute.tag);
        if (attr == null) {
            return paramNames;
        }

        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i = 0; i < attr.tableLength(); i++) {
            map.put(attr.index(i), i);
        }
        int index = 0;
        boolean isStaticMethod = javassist.Modifier.isStatic(behavior.getModifiers());
        boolean flag = false;
        for (Integer key : map.keySet()) {
            if (!isStaticMethod && !flag) {
                flag = true;
                continue;
            }

            if (index < paramNames.length) {
                paramNames[index++] = attr.variableName(map.get(key));
            } else {
                break;
            }
        }
        return paramNames;
    }

   public static Constructor chooseConstructor(Constructor<?>[] constructors, Boolean preferConstructorWithParams) {
        Constructor ctor = null;
        Constructor ctor1 = null;
        Constructor ctorWithNoParams = null;
        for (Constructor<?> constructor : constructors) {
            if (constructor.getParameterTypes().length == 0) {
                ctorWithNoParams = constructor;
            }
        }

        if (ctorWithNoParams != null && (preferConstructorWithParams == null || !preferConstructorWithParams)) {
            return ctorWithNoParams;
        }

        for (int i = 0; i < constructors.length; i++) {
            if (!MethodHelper.isDeprecated(constructors[i])) {
                ctor1 = constructors[i];
                if (constructors[i].getParameterTypes().length > 0) {
                    ctor = constructors[i];
                    break;
                }
            }
        }
        if (ctor == null) {
            ctor = ctor1;
        }
        return ctor;
    }

    static String filterMethodModifier(String modifier) {
        for (String s : excludeModifiers) {
            if (modifier.contains(s)) {
                modifier = modifier.replace(s, "");
            }
        }

        return CharMatcher.whitespace().collapseFrom(modifier, ' ').trim();
    }

    public static String getMethodString(Method method) {
        if (method == null) {
            return "";
        }
        String methodString = method.toString();
        int index = methodString.lastIndexOf('(');
        int spaceIndex = -1;
        for (int i = index - 1; i >= 0; i--) {
            if (methodString.charAt(i) == ' ') {
                spaceIndex = i;
                break;
            }
        }
        try {
            String fullMethodName = methodString.substring(spaceIndex + 1, index);
            String shortMethodName = fullMethodName.substring(fullMethodName.lastIndexOf('.') + 1);
            return methodString.substring(0, spaceIndex + 1) + shortMethodName + methodString.substring(index);
        } catch (Exception ex) {
            return method.toString();
        }

    }

    /**
     * 是否是泛型方法
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/7 14:12
     * @param method: 方法对象
     * @return: boolean true->是
     */
    public static boolean isGenericMode(Method method) {
        if (method.getTypeParameters().length > 0) {
            return false;
        }

        for (TypeVariable<? extends Class<?>> classTypeVariable : method.getDeclaringClass().getTypeParameters()) {
            String ctv = classTypeVariable.toString();
            for (Type type : method.getGenericParameterTypes()) {
                if (type.toString().equals(ctv) || type.toString().contains("<" + ctv + ">")) {
                    return false;
                }
            }
        }

        boolean b = false;
        for (Type type : method.getGenericParameterTypes()) {
            if (type.getTypeName().contains("<")) {
                b = true;
                break;
            }
        }
        if (!b) {
            return false;
        }
        return true;
    }

    /**
     * 获取参数字符串
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/7 14:29
     * @param argTypes: 参数类型
     * @param expectedVarNames: 期望的变量名
     * @param methodBody: 方法体
     * @param importClasses: 依赖对象名集合
     * @param variableNames: 变量名
     * @return: java.lang.String 参数字符串
     */
    public static String getArgs(Type[] argTypes, String[] expectedVarNames,StringBuilder methodBody,List<String> importClasses, VariableNames variableNames) {
        Objects.requireNonNull(argTypes);
        String args = "";
        for (int j = 0; j < argTypes.length; j++) {
            //构建类型值对象
            TypeValueCode valueClause = new TypeValueCode(argTypes[j].getTypeName(), importClasses, variableNames);
            String arg = valueClause.infer(expectedVarNames[j]);
            if (j < argTypes.length - 1) {
                args += arg + ", ";
            } else {
                args += arg;
            }
            appendLines(valueClause.clauses, 2,methodBody);
        }
        if (argTypes.length > 0) {
            methodBody.append(RT_1);
        }
        return args;
    }
    public static String getSimpleArgs(Class[] paramTypes, String[] expectedVarNames,StringBuilder methodBody,List<String> importClasses,VariableNames variableNames,boolean needDefineVal) {
        Objects.requireNonNull(paramTypes);
        String args = "";
        for (int i = 0; i < paramTypes.length; i++) {
            Class paramType = paramTypes[i];
            String paramTypeName = getClassName(paramType,importClasses);
            String variableName;
            if (expectedVarNames != null && expectedVarNames[i] != null) {
                variableName = variableNames.get(expectedVarNames[i]);
            } else {
                variableName = variableNames.get(paramType);
            }

            if (paramType.isPrimitive() || "String".equals(paramType.getSimpleName())) {
                methodBody.append(BLANK_8 + paramTypeName + " " + variableName + " = " + PrimitiveType.get(paramType) + ";" + RT_1);

            } else if (WrapperType.contains(paramType)) {
                methodBody.append(BLANK_8 + paramTypeName + " " + variableName + " = " + WrapperType.getValue(paramType) + ";" + RT_1);

            } else {
                Constructor constructor = null;
                if (!Modifier.isAbstract(paramType.getModifiers())) {
                    constructor = MethodHelper.chooseConstructor(paramType.getConstructors(), true);
                }

                //inner class
                boolean useCtor = true;
                if (paramType.getEnclosingClass() != null && !Modifier.isStatic(paramType.getModifiers())) {
                    useCtor = false;
                }
                if (constructor != null && useCtor) {
                    String ex = MethodHelper.needTryCatch(constructor);
                    if (ex != null) {
                        genInstanceName(methodBody, paramTypeName, variableName,needDefineVal);
                        methodBody.append(BLANK_8 + "try {" + RT_1
                            + BLANK_8 + BLANK_4 + variableName + " = new " + paramTypeName + "(" + getInvokeParameters(constructor.getParameterTypes(),importClasses) + ");" + RT_1);

                        String exceptionVarName = variableNames.get("ex", false);
                        methodBody.append(BLANK_8 + "} catch (" + ex + " " + exceptionVarName + ") {" + RT_1
                            + BLANK_8 + BLANK_4 + exceptionVarName + ".printStackTrace();" + RT_1
                            + BLANK_4 + BLANK_8 + "fail(\"Unexpected exception: \"" + " + " + exceptionVarName + ");" + RT_1 + BLANK_8 + "}" + RT_1);

                    } else {
                        String s = "new " + paramTypeName + "(" + getInvokeParameters(constructor.getParameterTypes(),importClasses) + ");";
                        methodBody.append(BLANK_8 + paramTypeName + " " + variableName + " = " + s + RT_1);
                    }
                } else {
                    genInstanceName(methodBody, paramTypeName, variableName,needDefineVal);
                }

            }
            args += variableName + ", ";
        }

        args = args.contains(",") ? args.substring(0, args.lastIndexOf(",")) : args;
        return args;
    }
    public static String getInvokeParameters(Class[] paramTypes,List<String> importClasses) {
        String params = "";
        for (int i = 0; i < paramTypes.length; i++) {
            if (paramTypes[i].getName().equals("byte")) {
                params += "(byte)" + PrimitiveType.get(paramTypes[i]) + ", ";

            } else if (paramTypes[i].getName().equals("short")) {
                params += "(short)" + PrimitiveType.get(paramTypes[i]) + ", ";
            } else {
                String s = PrimitiveType.get(paramTypes[i]);
                if (s.equals("null")) {
                    s = "(" + getClassName(paramTypes[i],importClasses) + ") " + s;
                }
                params += s + ", ";
            }
        }
        return params.contains(",") ? params.substring(0, params.lastIndexOf(",")) : params;
    }
    /**
     * 行累加
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/7 14:32
     * @param list: 待累加数据
     * @param tabCnt: 换行
     * @param methodSb: 方法体
     * @return: void
     */
    private static void appendLines(List<String> list, int tabCnt,StringBuilder methodSb) {
        String s = "";
        for (int i = 0; i < tabCnt; i++) {
            s += BLANK_4;
        }
        for (int i = 0; i < list.size(); i++) {
            methodSb.append(s + list.get(i) + RT_1);
        }
    }
    /**
     * 获取类名
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/7 14:33
     * @param clazz: 目标类
     * @param importClasses: 依赖对象名集合
     * @return: java.lang.String 类名
     */
    public static String getClassName(Class clazz,List<String> importClasses) {
        if (!clazz.getName().contains(".")) {
            return clazz.getTypeName();
        }
        String result = clazz.getTypeName();

        Class c = clazz;
        while (c.isArray()) {
            c = c.getComponentType();
        }

        Class outClass = c.getEnclosingClass();
        if (outClass != null) {
            String outClassName = outClass.getTypeName();
            return result.substring(0, outClassName.length()) + "." + result.substring(outClassName.length() + 1);
        }

        String className = c.getTypeName();
        String shortName = c.getSimpleName();

        boolean b = false;
        for (int i = 0; i < importClasses.size(); i++) {
            String importClass = importClasses.get(i);
            if (importClass.substring(importClass.lastIndexOf(".") + 1).equals(shortName) && !importClass.equals(className)) {
                b = true;
                break;
            }
        }
        if (b) {
            return result;
        }

        if (!importClasses.contains(className)) {
            importClasses.add(className);
        }
        return result.substring(result.lastIndexOf(".") + 1);
    }

    /**
     * 获取接口实现类
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/12 14:31
     * @param interfaceClazz: 接口类
     * @return: java.lang.Class 接口实现类
     */
    public static Class getImplClass(Class interfaceClazz) {
        ImplementFinder implementFinder = ExtensionLoader.getExtensionLoader(ImplementFinder.class).getDefaultExtension();
        Optional<List<Class>> optionalClassList = implementFinder.find(interfaceClazz);

        Class implClass = null;
        if(optionalClassList.isPresent() && !optionalClassList.get().isEmpty()){
            implClass = optionalClassList.get().get(0);
        }
        return implClass;
    }

    /**
     * 生成测试类变量
     * <li></li>
     * @author duanyong@jccfc.com
     * @date 2021/1/7 17:26
     * @param methodBody: 方法体
     * @param testClassName: 测试类名
     * @param instanceName: 测试类实例名
     * @param needDefineVal: 是否需要定义变量
     * @return: void
     */
    public static void genInstanceName(StringBuilder methodBody, String testClassName, String instanceName,boolean needDefineVal) {
        if(!needDefineVal){
            return;
        }
        methodBody.append(BLANK_8 + testClassName + BLANK_1 + instanceName + " = null;" + RT_1);
    }
}
